/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.mathematics;

/**
 * Class representing quaternions.
 */
public class Quaternion implements Cloneable {
	private MathVector values;
	
	/**
	 * Creates the quaternion x0+x1*i+x2*j+x3*k
	 * 
	 * @param values coefficients of the quaternion
	 * @throw IllegalArgument exception if the number of the given coefficients does not match four
	 */
	public Quaternion(double... values) {
		if (values.length != 4)
			throw new IllegalArgumentException("number of quaternion coefficients must match four");
		this.values = new MathVector(values);
	}
	
	/**
	 * Creates a unit quaternion describing a rotation of the given angle around the given axis. the resulting
	 * quaternion q can be used for rotations of three dimensional vectors x by the mapping q*x'*conj(q) where x'
	 * denotes the pure quaternion corresponding to x.
	 * 
	 * @param angle angle of the rotation (in radians)
	 * @param axis axis of the rotation (will be normalized if not already)
	 * @throw IllegalArgumentException if the given axis is not of dimension three
	 */
	public Quaternion(double angle, MathVector axis) {
		if (axis.dim != 3)
			throw new IllegalArgumentException("given axis must have dimension three");
		if (axis.norm() != 1.)
			axis.normalize();
		double cosAngle = Math.cos(angle / 2.);
		double sinAngle = Math.sin(angle / 2.);
		values = new MathVector(4);
		values.set(1, cosAngle);
		values.set(2, axis.get(1) * sinAngle);
		values.set(3, axis.get(2) * sinAngle);
		values.set(4, axis.get(3) * sinAngle);
	}
	
	/**
	 * Returns the absolute value of the quaternion.
	 * 
	 * @return absolute value of the quaternion
	 */
	public double abs() {
		return values.norm();
	}
	
	/**
	 * Adds the given quaternion to this one.
	 * 
	 * @param q quaternion to add
	 */
	public void add(Quaternion q) {
		values.add(q.values);
	}
	
	/**
	 * Returns a copy of this quaternion.
	 * 
	 * @return copy of this quaternion
	 */
	@Override
	public Quaternion clone() {
		Quaternion q = new Quaternion(values.clone().getElements());
		return q;
	}
	
	/**
	 * Returns the conjugation of this quaternion.
	 * 
	 * @return conjugation of this quaternion
	 */
	public Quaternion conj() {
		Quaternion q = this.clone();
		q.set(2, -q.get(2));
		q.set(3, -q.get(3));
		q.set(4, -q.get(4));
		return q;
	}
	
	/**
	 * Returns the specified coefficient of the quaternion.
	 * 
	 * @param i value from 1 to 4 (number of corresponding coefficient; numbering starts with real part)
	 * @return the specified coefficient of the quaternion
	 * @throw IllegalArgumentException if the given coefficient index is not a value from 1 to 4
	 */
	public double get(int i) {
		if (i < 1 || i > 4)
			throw new IllegalArgumentException("given coefficient index must not be larger than 4 or less than 1");
		return values.get(i);
	}
	
	/**
	 * This method tries to return the angle of the rotation described by this quaternion.
	 * <p>
	 * Note that the angle can only be returned for unit quaternions. Due to rounding errors, unit quaternions might
	 * become disrupted (not having length 1 anymore). Therefore, this method will normalize tha quaternion prior to any
	 * computations.
	 * 
	 * @return angle of rotation described by unit quaternion (in radians)
	 */
	public double getAngle() {
		normalize();
		double alpha = Math.acos(values.get(1));
		return 2. * alpha;
	}
	
	/**
	 * This method tries to return the axis of the rotation described by this quaternion.
	 * <p>
	 * Note that the axis can only be returned for unit quaternions. Due to rounding errors, unit quaternions might
	 * become disrupted (not having length 1 anymore). Therefore, this method will normalize tha quaternion prior to any
	 * computations.
	 * 
	 * @return axis of rotation described by unit quaternion (note that in the case of the identity rotation, the axis
	 *         [1., 0., 0.] will be returned)
	 */
	public MathVector getAxis() {
		double alpha = getAngle();
		if (alpha == 0.)
			return Mathematics.getUnitVector(3, 1);
		MathVector axis = new MathVector(values.get(2), values.get(3), values.get(4));
		axis.mul(1. / Math.sin(alpha / 2.));
		return axis;
	}
	
	/**
	 * Returns the unreal part of the quaternion.
	 * 
	 * @return unreal part of the quaternion
	 */
	public MathVector im() {
		return new MathVector(values.get(2), values.get(3), values.get(4));
	}
	
	/**
	 * Returns the inverse of this quaternion.
	 * 
	 * @return inverse of this quaternion
	 */
	public Quaternion inv() {
		Quaternion inv = conj();
		inv.mul(1. / sqrAbs());
		return inv;
	}
	
	/**
	 * Returns whether this is a unit quaternion (its absolute value equals one).
	 * 
	 * @return whether this is a unit quaternion
	 */
	public boolean isUnitQuaternion() {
		return (sqrAbs() == 1);
	}
	
	/**
	 * Multiplies the quaternion with a scalar.
	 */
	public void mul(double s) {
		values.mul(s);
	}
	
	/**
	 * Normalizes this quaternion such that it's absolute value equals 1 afterwards.
	 */
	public void normalize() {
		values.normalize();
	}
	
	/**
	 * Returns the real part of the quaternion.
	 * 
	 * @return real part of the quaternion
	 */
	public double re() {
		return values.get(1);
	}
	
	/**
	 * Sets the specified coefficient of the quaternion.
	 * 
	 * @param i value from 1 to 4 (number of corresponding coefficient; numbering starts with real part)
	 * @throw IllegalArgumentException if the given coefficient index is not a value from 1 to 4
	 */
	public void set(int i, double value) {
		if (i < 1 || i > 4)
			throw new IllegalArgumentException("given coefficient index must not be larger than 4 or less than 1");
		values.set(i, value);
	}
	
	/**
	 * Returns the sign of the quaternion, that is the quaternion in the same direction on the unit 4-dimensional
	 * hypersphere.
	 * 
	 * @return sign of the quaternion
	 */
	public Quaternion sgn() {
		Quaternion q = this.clone();
		q.normalize();
		return q;
	}
	
	/**
	 * Returns the squared absolute value of the quaternion.
	 * 
	 * @return squared absolute value of the quaternion
	 */
	public double sqrAbs() {
		return values.sqrNorm();
	}
	
	/**
	 * Subtracts the given quaternion to this one.
	 * 
	 * @param q quaternion to subtract
	 */
	public void sub(Quaternion q) {
		values.sub(q.values);
	}
	
	/**
	 * Returns a readable representation of this quaternion.
	 * 
	 * @return readable representation of this quaternion
	 */
	@Override
	public String toString() {
		String str = "";
		str += get(1) + " + ";
		str += get(2) + "*i + ";
		str += get(3) + "*j + ";
		str += get(4) + "*k";
		return str;
	}
}
